// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#ifdef ENABLE_QT_GUI
#include <QCoreApplication>
#include <QFileInfo>
#include <QProcess>
#include <QStandardPaths>
#include "qt_gui/sdl_event_wrapper.h"
#endif
#include "SDL3/SDL_events.h"
#include "SDL3/SDL_hints.h"
#include "SDL3/SDL_init.h"
#include "SDL3/SDL_properties.h"
#include "SDL3/SDL_timer.h"
#include "SDL3/SDL_video.h"
#include "common/assert.h"
#include "common/config.h"
#include "common/elf_info.h"
#include "core/debug_state.h"
#include "core/devtools/layer.h"
#include "core/libraries/kernel/time.h"
#include "core/libraries/pad/pad.h"
#include "core/libraries/system/userservice.h"
#include "imgui/renderer/imgui_core.h"
#include "input/controller.h"
#include "input/input_handler.h"
#include "input/input_mouse.h"
#include "sdl_window.h"
#include "video_core/renderdoc.h"

#ifdef __APPLE__
#include "SDL3/SDL_metal.h"
#endif
static bool pause_due_to_focus_loss = false;

namespace Frontend {

using namespace Libraries::Pad;

static OrbisPadButtonDataOffset SDLGamepadToOrbisButton(u8 button) {
    switch (button) {
    case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
        return OrbisPadButtonDataOffset::Down;
    case SDL_GAMEPAD_BUTTON_DPAD_UP:
        return OrbisPadButtonDataOffset::Up;
    case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
        return OrbisPadButtonDataOffset::Left;
    case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
        return OrbisPadButtonDataOffset::Right;
    case SDL_GAMEPAD_BUTTON_SOUTH:
        return OrbisPadButtonDataOffset::Cross;
    case SDL_GAMEPAD_BUTTON_NORTH:
        return OrbisPadButtonDataOffset::Triangle;
    case SDL_GAMEPAD_BUTTON_WEST:
        return OrbisPadButtonDataOffset::Square;
    case SDL_GAMEPAD_BUTTON_EAST:
        return OrbisPadButtonDataOffset::Circle;
    case SDL_GAMEPAD_BUTTON_START:
        return OrbisPadButtonDataOffset::Options;
    case SDL_GAMEPAD_BUTTON_TOUCHPAD:
        return OrbisPadButtonDataOffset::TouchPad;
    case SDL_GAMEPAD_BUTTON_BACK:
        return OrbisPadButtonDataOffset::TouchPad;
    case SDL_GAMEPAD_BUTTON_GUIDE:
        return OrbisPadButtonDataOffset::Home;
    case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:
        return OrbisPadButtonDataOffset::L1;
    case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER:
        return OrbisPadButtonDataOffset::R1;
    case SDL_GAMEPAD_BUTTON_LEFT_STICK:
        return OrbisPadButtonDataOffset::L3;
    case SDL_GAMEPAD_BUTTON_RIGHT_STICK:
        return OrbisPadButtonDataOffset::R3;
    default:
        return OrbisPadButtonDataOffset::None;
    }
}

static Uint32 SDLCALL PollController(void* userdata, SDL_TimerID timer_id, Uint32 interval) {
    auto* controller = reinterpret_cast<Input::GameController*>(userdata);
    return controller->Poll();
}

WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameControllers* controllers_,
                     std::string_view window_title)
    : width{width_}, height{height_}, controllers{*controllers_} {
    if (!SDL_SetHint(SDL_HINT_APP_NAME, "shadPS4")) {
        UNREACHABLE_MSG("Failed to set SDL window hint: {}", SDL_GetError());
    }
    if (!SDL_Init(SDL_INIT_VIDEO)) {
        UNREACHABLE_MSG("Failed to initialize SDL video subsystem: {}", SDL_GetError());
    }
    SDL_InitSubSystem(SDL_INIT_AUDIO);

    SDL_PropertiesID props = SDL_CreateProperties();
    SDL_SetStringProperty(props, SDL_PROP_WINDOW_CREATE_TITLE_STRING,
                          std::string(window_title).c_str());
    SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, SDL_WINDOWPOS_CENTERED);
    SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, SDL_WINDOWPOS_CENTERED);
    SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, width);
    SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, height);
    SDL_SetNumberProperty(props, "flags", SDL_WINDOW_VULKAN);
    SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true);
    window = SDL_CreateWindowWithProperties(props);
    SDL_DestroyProperties(props);
    if (window == nullptr) {
        UNREACHABLE_MSG("Failed to create window handle: {}", SDL_GetError());
    }

    SDL_SetWindowMinimumSize(window, 640, 360);

    bool error = false;
    const SDL_DisplayID displayIndex = SDL_GetDisplayForWindow(window);
    if (displayIndex < 0) {
        LOG_ERROR(Frontend, "Error getting display index: {}", SDL_GetError());
        error = true;
    }
    const SDL_DisplayMode* displayMode;
    if ((displayMode = SDL_GetCurrentDisplayMode(displayIndex)) == 0) {
        LOG_ERROR(Frontend, "Error getting display mode: {}", SDL_GetError());
        error = true;
    }
    if (!error) {
        SDL_SetWindowFullscreenMode(
            window, Config::getFullscreenMode() == "Fullscreen" ? displayMode : NULL);
    }
    SDL_SetWindowFullscreen(window, Config::getIsFullscreen());

    SDL_InitSubSystem(SDL_INIT_GAMEPAD);

#if defined(SDL_PLATFORM_WIN32)
    window_info.type = WindowSystemType::Windows;
    window_info.render_surface = SDL_GetPointerProperty(SDL_GetWindowProperties(window),
                                                        SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
#elif defined(SDL_PLATFORM_LINUX)
    if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "x11") == 0) {
        window_info.type = WindowSystemType::X11;
        window_info.display_connection = SDL_GetPointerProperty(
            SDL_GetWindowProperties(window), SDL_PROP_WINDOW_X11_DISPLAY_POINTER, NULL);
        window_info.render_surface = (void*)SDL_GetNumberProperty(
            SDL_GetWindowProperties(window), SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0);
    } else if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "wayland") == 0) {
        window_info.type = WindowSystemType::Wayland;
        window_info.display_connection = SDL_GetPointerProperty(
            SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WAYLAND_DISPLAY_POINTER, NULL);
        window_info.render_surface = SDL_GetPointerProperty(
            SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WAYLAND_SURFACE_POINTER, NULL);
    }
#elif defined(SDL_PLATFORM_MACOS)
    window_info.type = WindowSystemType::Metal;
    window_info.render_surface = SDL_Metal_GetLayer(SDL_Metal_CreateView(window));
#endif
    // input handler init-s
    Input::ControllerOutput::LinkJoystickAxes();
    Input::ParseInputConfig(std::string(Common::ElfInfo::Instance().GameSerial()));
    Input::GameControllers::TryOpenSDLControllers(controllers);

    if (Config::getBackgroundControllerInput()) {
        SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
    }
}

WindowSDL::~WindowSDL() {
    if (window) {
        SDL_DestroyWindow(window);
    }
}
SDL_Event* e = nullptr;

void WindowSDL::WaitEvent() {
    // Called on main thread
    SDL_Event event;
    SDL_memset(&event, 0, sizeof(SDL_Event));

    if (!SDL_WaitEvent(&event)) {
        return;
    }

#ifdef ENABLE_QT_GUI
    if (SdlEventWrapper::Wrapper::wrapperActive) {
        if (SdlEventWrapper::Wrapper::GetInstance()->ProcessEvent(&event))
            return;
    }
#endif

    if (ImGui::Core::ProcessEvent(&event)) {
        return;
    }
    switch (event.type) {
    case SDL_EVENT_WINDOW_FOCUS_LOST:
        if (Config::getPauseOnUnfocus()) {
            if (!DebugState.IsGuestThreadsPaused()) {
                DebugState.PauseGuestThreads();
                pause_due_to_focus_loss = true;
            }
        }
        break;
    case SDL_EVENT_WINDOW_FOCUS_GAINED:
        if (Config::getPauseOnUnfocus()) {

            if (pause_due_to_focus_loss) {
                DebugState.ResumeGuestThreads();
                pause_due_to_focus_loss = false;
            }
        }
        break;
    case SDL_EVENT_WINDOW_RESIZED:
    case SDL_EVENT_WINDOW_MAXIMIZED:
    case SDL_EVENT_WINDOW_RESTORED:
        OnResize();
        break;
    case SDL_EVENT_WINDOW_MINIMIZED:
    case SDL_EVENT_WINDOW_EXPOSED:
        is_shown = event.type == SDL_EVENT_WINDOW_EXPOSED;
        OnResize();
        break;
    case SDL_EVENT_MOUSE_BUTTON_DOWN:
    case SDL_EVENT_MOUSE_BUTTON_UP:
    case SDL_EVENT_MOUSE_WHEEL:
    case SDL_EVENT_MOUSE_WHEEL_OFF:
    case SDL_EVENT_KEY_DOWN:
    case SDL_EVENT_KEY_UP:
        OnKeyboardMouseInput(&event);
        break;
    case SDL_EVENT_GAMEPAD_ADDED:
    case SDL_EVENT_GAMEPAD_REMOVED:
        // todo handle userserviceevents here
        Input::GameControllers::TryOpenSDLControllers(controllers);
        break;
    case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
    case SDL_EVENT_GAMEPAD_BUTTON_UP:
    case SDL_EVENT_GAMEPAD_AXIS_MOTION:
    case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN:
    case SDL_EVENT_GAMEPAD_TOUCHPAD_UP:
    case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION:
        OnGamepadEvent(&event);
        break;
    case SDL_EVENT_GAMEPAD_SENSOR_UPDATE:
        OnGamepadEvent(&event);
        break;
    case SDL_EVENT_QUIT:
        is_open = false;
        break;
    case SDL_EVENT_QUIT + 1:
        is_open = false;
        RelaunchEmulator();
        break;

    case SDL_EVENT_QUIT_DIALOG:
        Overlay::ToggleQuitWindow();
        break;
    case SDL_EVENT_TOGGLE_FULLSCREEN: {
        if (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) {
            SDL_SetWindowFullscreen(window, 0);
        } else {
            SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN);
        }
        break;
    }
    case SDL_EVENT_TOGGLE_PAUSE:
        if (DebugState.IsGuestThreadsPaused()) {
            LOG_INFO(Frontend, "Game Resumed");
            DebugState.ResumeGuestThreads();
        } else {
            LOG_INFO(Frontend, "Game Paused");
            DebugState.PauseGuestThreads();
        }
        break;
    case SDL_EVENT_CHANGE_CONTROLLER:
        UNREACHABLE_MSG("todo");
        break;
    case SDL_EVENT_TOGGLE_SIMPLE_FPS:
        Overlay::ToggleSimpleFps();
        break;
    case SDL_EVENT_RELOAD_INPUTS:
        Input::ParseInputConfig(std::string(Common::ElfInfo::Instance().GameSerial()));
        break;
    case SDL_EVENT_MOUSE_TO_JOYSTICK:
        SDL_SetWindowRelativeMouseMode(this->GetSDLWindow(),
                                       Input::ToggleMouseModeTo(Input::MouseMode::Joystick));
        break;
    case SDL_EVENT_MOUSE_TO_GYRO:
        SDL_SetWindowRelativeMouseMode(this->GetSDLWindow(),
                                       Input::ToggleMouseModeTo(Input::MouseMode::Gyro));
        break;
    case SDL_EVENT_ADD_VIRTUAL_USER:
        for (int i = 0; i < 4; i++) {
            if (controllers[i]->user_id == -1) {
                controllers[i]->user_id = i + 1;
                Libraries::UserService::AddUserServiceEvent(
                    {Libraries::UserService::OrbisUserServiceEventType::Login,
                     (s32)controllers[i]->user_id});
                break;
            }
        }
        break;
    case SDL_EVENT_REMOVE_VIRTUAL_USER:
        LOG_INFO(Input, "Remove user");
        for (int i = 3; i >= 0; i--) {
            if (controllers[i]->user_id != -1) {
                Libraries::UserService::AddUserServiceEvent(
                    {Libraries::UserService::OrbisUserServiceEventType::Logout,
                     (s32)controllers[i]->user_id});
                controllers[i]->user_id = -1;
                break;
            }
        }
        break;
    case SDL_EVENT_MOUSE_TO_TOUCHPAD:
        SDL_SetWindowRelativeMouseMode(this->GetSDLWindow(),
                                       Input::ToggleMouseModeTo(Input::MouseMode::Touchpad));
        SDL_SetWindowRelativeMouseMode(this->GetSDLWindow(), false);
        break;
    case SDL_EVENT_RDOC_CAPTURE:
        VideoCore::TriggerCapture();
        break;
    default:
        break;
    }
}

void WindowSDL::RelaunchEmulator() {
#ifdef Q_OS_WIN
    QString emulatorPath = QCoreApplication::applicationFilePath(); // Get current executable path
    QString emulatorDir = QFileInfo(emulatorPath).absolutePath();   // Get working directory
    QString scriptFileName =
        QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/relaunch.ps1";

    // Use double quotes, remove unnecessary escaping
    QString scriptContent =
        QStringLiteral("Start-Sleep -Seconds 2\n"
                       "Start-Process -FilePath \"%1\" -WorkingDirectory \"%2\"\n")
            .arg(emulatorPath, emulatorDir);

    QFile scriptFile(scriptFileName);
    if (scriptFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
        QTextStream out(&scriptFile);
        scriptFile.write("\xEF\xBB\xBF"); // UTF-8 BOM for PowerShell script
        out << scriptContent;
        scriptFile.close();

        bool started =
            QProcess::startDetached("powershell.exe", QStringList() << "-ExecutionPolicy"
                                                                    << "Bypass"
                                                                    << "-File" << scriptFileName);
        if (!started) {
            qWarning() << "Failed to start relaunch PowerShell script";
        }
    } else {
        qWarning() << "Failed to write relaunch PowerShell script";
    }

#elif defined(Q_OS_LINUX) || defined(Q_OS_MAC)
    QString emulatorPath = QCoreApplication::applicationFilePath();
    QString scriptFileName = "/tmp/relaunch.sh";

    // Use full absolute path
    QString scriptContent = QStringLiteral("#!/bin/bash\n"
                                           "sleep 2\n"
                                           "exec \"%1\" \"$@\" &\n")
                                .arg(emulatorPath);

    QFile scriptFile(scriptFileName);
    if (scriptFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
        QTextStream out(&scriptFile);
        out << scriptContent;
        scriptFile.close();

        // Give full rwx for owner, and rx for others (safe for /tmp usage)
        scriptFile.setPermissions(QFileDevice::ExeOwner | QFileDevice::ReadOwner |
                                  QFileDevice::WriteOwner | QFileDevice::ExeGroup |
                                  QFileDevice::ReadGroup | QFileDevice::ExeOther |
                                  QFileDevice::ReadOther);

        bool started = QProcess::startDetached("bash", QStringList() << scriptFileName);
        if (!started) {
            qWarning() << "Failed to start relaunch bash script";
        }
    } else {
        qWarning() << "Failed to write relaunch bash script";
    }
#endif
}

void WindowSDL::InitTimers() {
    for (int i = 0; i < 4; i++) {
        SDL_AddTimer(250, &PollController, controllers[i]);
    }
    SDL_AddTimer(33, Input::MousePolling, (void*)controllers[0]);
}

void WindowSDL::RequestKeyboard() {
    if (keyboard_grab == 0) {
        SDL_RunOnMainThread(
            [](void* userdata) { SDL_StartTextInput(static_cast<SDL_Window*>(userdata)); }, window,
            true);
    }
    keyboard_grab++;
}

void WindowSDL::ReleaseKeyboard() {
    ASSERT(keyboard_grab > 0);
    keyboard_grab--;
    if (keyboard_grab == 0) {
        SDL_RunOnMainThread(
            [](void* userdata) { SDL_StopTextInput(static_cast<SDL_Window*>(userdata)); }, window,
            true);
    }
}

void WindowSDL::OnResize() {
    SDL_GetWindowSizeInPixels(window, &width, &height);
    ImGui::Core::OnResize();
}

Uint32 wheelOffCallback(void* og_event, Uint32 timer_id, Uint32 interval) {
    SDL_Event off_event = *(SDL_Event*)og_event;
    off_event.type = SDL_EVENT_MOUSE_WHEEL_OFF;
    SDL_PushEvent(&off_event);
    delete (SDL_Event*)og_event;
    return 0;
}

void WindowSDL::OnKeyboardMouseInput(const SDL_Event* event) {
    using Libraries::Pad::OrbisPadButtonDataOffset;

    // get the event's id, if it's keyup or keydown
    const bool input_down = event->type == SDL_EVENT_KEY_DOWN ||
                            event->type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
                            event->type == SDL_EVENT_MOUSE_WHEEL;
    Input::InputEvent input_event = Input::InputBinding::GetInputEventFromSDLEvent(*event);

    // if it's a wheel event, make a timer that turns it off after a set time
    if (event->type == SDL_EVENT_MOUSE_WHEEL) {
        SDL_Event* copy = new SDL_Event(*event);
        if (!SDL_AddTimer(33, wheelOffCallback, (void*)copy)) {
            delete copy;
        }
    }

    // add/remove it from the list
    bool inputs_changed = Input::UpdatePressedKeys(input_event);

    // update bindings
    if (inputs_changed) {
        Input::ActivateOutputsFromInputs();
    }
}

void WindowSDL::OnGamepadEvent(const SDL_Event* event) {
    bool input_down = false;
    if (event->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN)
        input_down = true;
    else if (event->type == SDL_EVENT_GAMEPAD_BUTTON_UP)
        input_down = false;

    Input::InputEvent input_event = Input::InputBinding::GetInputEventFromSDLEvent(*event);

    if (event->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN ||
        event->type == SDL_EVENT_GAMEPAD_BUTTON_UP) {
        int idx = Input::GameControllers::GetGamepadIndexFromJoystickId(event->gbutton.which,
                                                                        controllers);

        if (event->gbutton.button == SDL_GAMEPAD_BUTTON_TOUCHPAD) {
            controllers[idx]->CheckButton(0, OrbisPadButtonDataOffset::TouchPad, input_down);
            return;
        }

        if (event->gbutton.button == SDL_GAMEPAD_BUTTON_GUIDE) {
            controllers[idx]->CheckButton(0, OrbisPadButtonDataOffset::Home, input_down);

            if (Config::DisableHardcodedHotkeys() && event->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
                SDL_Event quit_event{};
                quit_event.type = SDL_EVENT_QUIT_DIALOG;
                SDL_PushEvent(&quit_event);
            }
        }
    }

    switch (event->type) {
    case SDL_EVENT_GAMEPAD_SENSOR_UPDATE: {
        if (!Config::getIsMotionControlsEnabled())
            return;
        int idx = Input::GameControllers::GetGamepadIndexFromJoystickId(event->gsensor.which,
                                                                        controllers);
        switch ((SDL_SensorType)event->gsensor.sensor) {
        case SDL_SENSOR_GYRO:
            controllers[idx]->Gyro(0, event->gsensor.data);
            break;
        case SDL_SENSOR_ACCEL:
            controllers[idx]->Acceleration(0, event->gsensor.data);
            break;
        default:
            break;
        }
        return;
    }
    case SDL_EVENT_GAMEPAD_TOUCHPAD_DOWN:
    case SDL_EVENT_GAMEPAD_TOUCHPAD_UP:
    case SDL_EVENT_GAMEPAD_TOUCHPAD_MOTION: {
        int idx = Input::GameControllers::GetGamepadIndexFromJoystickId(event->gtouchpad.which,
                                                                        controllers);
        controllers[idx]->SetTouchpadState(event->gtouchpad.finger,
                                           event->type != SDL_EVENT_GAMEPAD_TOUCHPAD_UP,
                                           event->gtouchpad.x, event->gtouchpad.y);
        return;
    }
    default:
        break;
    }

    // Standard input handling
    bool inputs_changed = Input::UpdatePressedKeys(input_event);
    if (inputs_changed)
        Input::ActivateOutputsFromInputs();
}

} // namespace Frontend
